import pdb
from random import random

class Player(): # Interfaces with the game, creates RoundInfo instances, and 
                # calls InternalPlayer twice per round (pre() and post()).

  def __init__(self):
    self.name = "Samm El-Harry"
    self.rounds = []
    self.current_round = None
    self.internal_player = InternalPlayer()
    pass

  def hunt_choices(self, round_number, current_food,
                   current_reputation, m, player_reputations):
    previous_round = self.rounds[-1] if len(self.rounds) > 0 else None
    self.current_round = RoundInfo(round_number, current_food,
                                   current_reputation, m, player_reputations,
                                   previous_round)
    self.rounds.append(self.current_round)
    res = self.internal_player.pre(self.current_round)
    self.current_round.my_choices = res
    return res

  def hunt_outcomes(self, food_earnings):
    choices = []
    for payout in food_earnings:
      choices.append({1 : 'h', -2: 's', 0 : 'h', -3: 's'}[payout])
    self.current_round.choices_with_me = choices
    self.current_round.payouts = food_earnings

  def round_end(self, award, m, number_hunters):
    self.current_round.ending_food = self.current_round.starting_food + \
                                     sum(self.current_round.payouts) + award
    self.current_round.award = award
    self.current_round.hunts = number_hunters
    self.internal_player.post(self.current_round)



class InternalPlayer(): # Our actual player class. Contains logic to track
                        # players, and calls on individual strategies as needed.

  def __init__(self):
    self.other_players = []

  def pre(self, round_info):

    # Create a new array of the other players, trying to keep ones from the
    # previous round if we can "lock on them" (make sure they are the same
    # player based on their reputation). For those we can't "lock on", create
    # brand new other players.
    tmp_other_players = [None] * len(round_info.reputations)
    for op in self.other_players:
      if op == None:
        continue
      candidates = []
      min_possible_hunts, max_possible_hunts = op.possible_hunts_range(round_info)        
      for i, rep in enumerate(round_info.reputations):
        if round_info.hunts_from_rep(rep) >= min_possible_hunts and \
            round_info.hunts_from_rep(rep) <= max_possible_hunts:
          candidates.append(i)
      if len(candidates) == 1: # Success!
        selected = candidates[0]
        tmp_other_players[selected] = op
        op.locked_on = True # And could already be locked on.
        # print "(a) Matched %d to %d." % (selected, self.other_players.index(op))
        self.other_players[self.other_players.index(op)] = None
    for i, rep in enumerate(round_info.reputations):
      candidates = []
      for op in self.other_players:
        if op == None:
          continue
        min_possible_hunts, max_possible_hunts = op.possible_hunts_range(round_info)
        if round_info.hunts_from_rep(rep) >= min_possible_hunts and \
            round_info.hunts_from_rep(rep) <= max_possible_hunts:
          candidates.append(op)
      if len(candidates) == 1: # Success!
        selected = candidates[0]
        tmp_other_players[i] = selected
        selected.locked_on = True # And could already be locked on.
        # print "(b) Matched %d to %d." % (i, self.other_players.index(selected))
        self.other_players[self.other_players.index(selected)] = None
    for i in range(len(tmp_other_players)):
      if tmp_other_players[i] == None:
        tmp_other_players[i] = OtherPlayer(round_info.round_number)
    self.other_players = tmp_other_players
    
    # s = 0
    # for op in self.other_players:
    #   s += 1 if op.locked_on else 0
    # print "Round %d. Tracked %d." % (round_info.round_number, s)

    # Update the players with their reputation.
    for i, op in enumerate(self.other_players):
      op.set_rep(round_info.reputations[i])

    # Divide players into two groups, those we've locked on and the rest. We 
    # will apply a strategy independently for each group, and recollect the
    # results.
    if len(self.other_players) == 1:
      res = AlwaysSlackStrategy.call(self.other_players, round_info.round_number)
    elif round_info.round_number <= 20:
      res = AlwaysHuntStrategy.call(self.other_players, round_info.round_number)
    else:
      locked_on_players = []
      non_locked_on_players = []
      for op in self.other_players:
        if op.locked_on:
          locked_on_players.append(op)
        else:
          non_locked_on_players.append(op)
      res_locked_on = TitForTatWithMaxRepStrategy.call(locked_on_players, round_info.round_number)
      res_non_locked_on = AlwaysSlackStrategy.call(non_locked_on_players, round_info.round_number)
      res = []
      for op in self.other_players:
        if op.locked_on:
          res.append(res_locked_on.pop(0))
        else:
          res.append(res_non_locked_on.pop(0))

    # Save what we played with every player.
    for i, op in enumerate(self.other_players):
      op.set_my_last_move(res[i])

    # Finally, return our choices.
    return res

  def post(self, round_info):

    # Save what every player played with us.
    for i, op in enumerate(self.other_players):
      op.set_last_move_with_me(round_info.choices_with_me[i])


class MatchingTitForTatStrategy():

  def call(players, round_number):
    res = []
    for p in players:
      if (round_number - p.created_in_round_number) < 0:
        res.append('h')
      elif p.recent_rep_with_me() > p.rep:
        res.append(p.last_move_with_me)
      else:
        res.append('s')
    return res
  call = staticmethod(call)


class TitForTatWithMaxRepStrategy(): # Play tit-for-tat with players that have
                                     # the top 20% of reputation with me. Make
                                     # hunts less probable as game goes forward.

  def call(players, round_number):
    if len(players) == 0:
      return []
    reps_with_me = []
    for p in players:
      reps_with_me.append(p.recent_rep_with_me())
    reps_with_me.sort()
    N = len(players) / 5
    N = 1 if N < 1 else N
    threshold = reps_with_me[-N] if len(reps_with_me) >= N else reps_with_me[0]
    threshold = 0.66 if threshold < 0.66 else threshold
    res = []
    for p in players:
      if (round_number - p.created_in_round_number) < 2:
        res.append('h')
      elif p.recent_rep_with_me() > 0.99 or p.recent_rep_with_me() >= threshold:
        res.append(p.last_move_with_me)
      else:
        res.append('s')
    res = ['h' if r == 'h' and random() < 2.0 / (round_number ** 0.5) else 's' for r in res]
    return res
  call = staticmethod(call)

class DecayMaxRepHunterStrategy():

  def call(players, round_number):
    if len(players) == 0:
      return []
    reps = []
    for p in players:
      reps.append(p.rep)
    threshold = max(reps)
    return ['h' if rep == threshold and random() < 1.0/round_number else 's' for rep in reps]
  call = staticmethod(call)

class AlwaysHuntStrategy():

  def call(players, round_number):
    return ['h'] * len(players)
  call = staticmethod(call)

class AlwaysSlackStrategy():

  def call(players, round_number):
    return ['s'] * len(players)
  call = staticmethod(call)


class RoundInfo(): # Class containing information about a round.

  def __init__(self, round_number, starting_food, starting_reputation, m,
               reputations, previous_round):
    # Some information can be set when the round is initiated.
    self.round_number = round_number
    self.starting_food = starting_food
    self.m = m
    self.reputations = reputations
    self.matchs = len(self.reputations)
    self.previous_round = previous_round
    if previous_round:
      self.rep_denominator = self.previous_round.rep_denominator + \
                             self.previous_round.matchs
    else:
      self.rep_denominator = 0
    # Others are filled in later.
    self.ending_food = None
    self.choices_with_me = None
    self.my_choices = None
    self.payouts = None
    self.award = None
    self.hunts = None # Number of hunts in this round.

  def hunts_from_rep(self, rep):
    return int(round(rep * self.rep_denominator))

class OtherPlayer(): # Class representing one player that isn't us, possibly
                     # tracking information from round to the next if the player
                     # is locked on.

  def __init__(self, created_in_round_number):
    self.created_in_round_number = created_in_round_number
    self.rep = None
    self.locked_on = False
    self.reps = [] # Reputations for every round since created.
    self.my_last_move = None
    self.my_moves = []
    self.last_move_with_me = None
    self.moves_with_me = []

  def set_rep(self, rep):
    self.rep = rep
    self.reps.append(rep)

  def set_my_last_move(self, move):
    self.my_last_move = move
    self.my_moves.append(move)

  def set_last_move_with_me(self, move):
    self.last_move_with_me = move
    self.moves_with_me.append(move)

  def possible_hunts_range(self, round_info): # Assumes it is called before
                                              # set_rep() is called for the
                                              # current round.
    previous_round_info = round_info.previous_round
    previous_round_rep = self.reps[-1]
    hunts = previous_round_rep * previous_round_info.rep_denominator
    min_possible_hunts = int(round(hunts))
    max_possible_hunts = int(round(hunts + previous_round_info.matchs))
    return (min_possible_hunts, max_possible_hunts)

  def recent_rep_with_me(self): # Weighted average of reputation with me.
    hunts = 0
    slacks = 0
    a = 0.75
    for i in range(len(self.moves_with_me)):
      move = self.moves_with_me[len(self.moves_with_me) - 1 - i]
      hunts += (a ** i) if move == 'h' else 0
      slacks += (a ** i) if move == 's' else 0
    return float(hunts) / (hunts + slacks) if hunts > 0 else 0.0

